package com.scope.imageFilters;

import java.util.ArrayList;
import java.util.List;

import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfInt;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.RotatedRect;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;

import android.content.Context;
import android.util.Log;

public class EdgeDetection extends BaseFilter<Object> {
	public EdgeDetection(Context context) {
		super(context);
		mName = getFilterName();
	}

	public static String getFilterName() {
		return "EdgeDetection";
	}

	@Override
	protected void applyFilter() {
		Mat blur = MatZeros();
		Imgproc.medianBlur(mSourceImageMat, blur, 9);

		List<Mat> blurList = new ArrayList<Mat>(1);
		blurList.add(blur);
		List<Mat> channelList = new ArrayList<Mat>(3);
		for (int i = 0; i < 3; i++) {
			channelList.add(i, MatZeros());
			Imgproc.cvtColor(channelList.get(i), channelList.get(i), Imgproc.COLOR_RGB2GRAY);
		}
		MatOfInt fromTo = new MatOfInt(0, 0, 1, 1, 2, 2);
		Core.mixChannels(blurList, channelList, fromTo);

		MatOfPoint maxRect = findMaxRectangle(channelList);
		autoRotate(maxRect);
	}

	public final static String TAG = getFilterName();

	// Main Edge Detection Method
	private MatOfPoint findMaxRectangle(List<Mat> matList) {
		List<MatOfPoint> rectangles = new ArrayList<MatOfPoint>();

		for (Mat mat : matList) {
			Mat cannyTemp = MatZeros();
			Imgproc.Canny(mat, cannyTemp, 20, 30);
			Mat canny = MatZeros();
			Imgproc.dilate(cannyTemp, canny, Mat.ones(new Size(3, 3), 0));
			Log.v(TAG, "Canny Done!");
			findRectangles(canny, rectangles);

			Mat thresh = MatZeros();
			Imgproc.adaptiveThreshold(mat, thresh, 255,
					Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 5, 5);

			// Perform erode operation
			Mat tempErode = MatZeros();
			Imgproc.erode(thresh, tempErode, Mat.ones(new Size(2, 2), 0));
			Imgproc.erode(tempErode, thresh, Mat.ones(new Size(2, 2), 0));

			Log.v(TAG, "Thresholding Done!");
			findRectangles(thresh, rectangles);
		}

		if (rectangles.isEmpty()) {
			return null;
		} else {
			// Find rectangular contour with biggest area
			double maxarea = 0;
			int maxareaidx = 0;
			for (int idx = 0; idx < rectangles.size(); ++idx) {
				double area = Math.abs(Imgproc.contourArea(rectangles.get(idx)));
				if (area > maxarea) {
					maxarea = area;
					maxareaidx = idx;
				}
			}

			Log.v(TAG, "Max Area calculated!" + maxarea);
			Log.v(TAG, "Biggest Contour Index" + maxareaidx);

			return rectangles.get(maxareaidx);
		}
	}

	// Retrieve all the contours and store them in a list
	private void findRectangles(Mat gray, List<MatOfPoint> rectangles) {
		List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
		Imgproc.findContours(gray, contours, new Mat(), 1, 2);
		Log.v(TAG, "Contours Found!");

		MatOfPoint2f contourApprox = new MatOfPoint2f();
		MatOfPoint2f contour2f = new MatOfPoint2f();
		
		for (int i = 0; i < contours.size(); i++) {
			contours.get(i).convertTo(contour2f, CvType.CV_32FC2);
			Imgproc.approxPolyDP(
					contour2f,
					contourApprox,
					Imgproc.arcLength(contour2f, true) * ARCLENGTH_FACTOR,
					true);

			MatOfPoint contour = isRectangle(contourApprox);
			if (contour != null) {
				rectangles.add(contour);
			}
		}
	}
	
	// Check for basic card conditions in each closed contour
	// return null if contour is not a rectangle
	private MatOfPoint isRectangle(MatOfPoint2f contourApprox) {
		MatOfPoint contour = new MatOfPoint();
		contourApprox.convertTo(contour, CvType.CV_32S);
		double areaPct = Imgproc.contourArea(contourApprox) / mSourceImageMat.total();
		
		if (areaPct < AREA_MAX_PCT && areaPct > AREA_MIN_PCT &&
				contourApprox.rows() == 4 && Imgproc.isContourConvex(contour)) {
			// Log.v(TAG,"Passes Conditions! " + approx.size().toString());
			double maxcosine = 0;
			Point[] corners = contourApprox.toArray();
			boolean rectangleFlag = true;
			double temp_cosine = 0;
			for (int j = 2; j < 5 && rectangleFlag; j++) {
				double cosine = Math.abs(findAngle(corners[j % 4],
						corners[j - 2], corners[j - 1]));
				if (j == 2) {
					temp_cosine = cosine;
				} else if ((cosine > (temp_cosine + 0.05))
						|| (cosine < (temp_cosine - 0.05))) {
					rectangleFlag = false;
				} else {
				}
				// Log.v(TAG,"Cosine of side " + j + " = " + cosine);
				maxcosine = Math.max(maxcosine, cosine);
			}

			if (rectangleFlag && (maxcosine < COSINE_THRESHOLD)) {
				return contour;
			}
		}
		
		return null;
	}

	// Calculates angles between adjacent sides given three points
	private double findAngle(Point point1, Point point2, Point point0) {
		double dx1 = point1.x - point0.x;
		double dy1 = point1.y - point0.y;
		double dx2 = point2.x - point0.x;
		double dy2 = point2.y - point0.y;
		return (dx1 * dx2 + dy1 * dy2)
				/ Math.sqrt((dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2)
						+ 1e-10);
	}

	private void autoRotate(MatOfPoint edges)
	{	
		if (edges == null) {
			mFilteredImageMat = mSourceImageMat;
			return;
		}
		
		int isNegative = 0;

		MatOfPoint2f forRotatedRect = new MatOfPoint2f();
		edges.convertTo(forRotatedRect, CvType.CV_32FC2);
		RotatedRect rotated = Imgproc.minAreaRect(forRotatedRect);
		
		Log.v(TAG,"Rotated Rect Received" + rotated.angle);
		Log.v(TAG,"Rotated Rect: " + rotated.center.x + rotated.center.y);
		double angle = rotated.angle;
		Size boxSize = rotated.size;
		
		Point[] corners = new Point[4];
		rotated.points(corners);
		Log.v(TAG,"Corner Top Left: "+ corners[2].x + " " + corners[2].y);
		for(int j = 0; j < 4; ++j){
			if(corners[j].x < 0 || corners[j].y < 0){
				isNegative = 1;
			}
		}
		
		if (isNegative == 1) {
			mFilteredImageMat = mSourceImageMat;
		}
		
		//Check corners and orientation of box and correct to horizontal
		if(angle<-45.0)
		{
			angle = angle + 90.0;
			double temp = boxSize.width;
			boxSize.width = boxSize.height;
			boxSize.height = temp;
		}	
		
		if(boxSize.width < boxSize.height){
			if(corners[1].y < corners[3].y){
				angle = angle + 90;
			} else{
				angle = angle - 90;
			}
			double temp = boxSize.width;
			boxSize.width = boxSize.height;
			boxSize.height = temp;
		}
		
		Log.v(TAG,"Final Angle Desired: " + angle);
		
		Mat transform = Imgproc.getRotationMatrix2D(rotated.center, angle, 1.0);
		Mat rotatedMat = MatZeros();
		
		Log.v(TAG,"Size: " + boxSize.width + " " + boxSize.height);
		
		//Perform rotation operation
		Imgproc.warpAffine(mSourceImageMat, rotatedMat, transform,
				mSourceImageMat.size(),Imgproc.INTER_CUBIC);
		
		Log.v(TAG,"RotatedMat found " + rotatedMat.total());
		
		rotatedMat.convertTo(rotatedMat, 0);
		
		Log.v(TAG,"Size of patch: " + boxSize.height + " " + boxSize.width);
		Log.v(TAG,"Center of patch: "+ rotated.center.x + " " + rotated.center.y);
	
		
		//Specify ROI for cropping the image
		double ht = boxSize.height;
		double wdth = boxSize.width;
		Point centre_point = rotated.center;
		Point[] p = new Point[4];
		p[0] = new Point(centre_point.x - (wdth/2), centre_point.y + (ht/2));
		p[1] = new Point(centre_point.x - (wdth/2), centre_point.y - (ht/2));
		p[2] = new Point(centre_point.x + (wdth/2), centre_point.y - (ht/2));
		p[3] = new Point(centre_point.x + (wdth/2), centre_point.y + (ht/2));
		
		Rect rect = new Rect((int)p[1].x,(int)p[1].y,(int)wdth,(int) ht);
		mFilteredImageMat = new Mat(rotatedMat, rect);
	}
	
	private final static double ARCLENGTH_FACTOR = 0.02;
	private final static double AREA_MAX_PCT = 0.88;
	private final static double AREA_MIN_PCT = 0.11;
	private final static double COSINE_THRESHOLD = 0.2;
}